/******************************************************************************

	COPYRIGHT(C) JONAS 'SORTIE' TERMANSEN 2010.

	This file is part of Maxsi Engine.

	Maxsi Engine is free software: you can redistribute it and/or modify it
	under the terms of the GNU Lesser General Public License as published by
	the Free Software Foundation, either version 3 of the License, or (at your
	option) any later version.

	Maxsi Engine is distributed in the hope that it will be useful, but WITHOUT
	ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
	FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
	for more details.

	You should have received a copy of the GNU Lesser General Public License
	along with Maxsi Engine. If not, see <http://www.gnu.org/licenses/>.

	MaxsiEngine.dll
	A general purpose C++ library for the Maxsi Technology Project.

	MaxsiWorker.cpp	
	A multithreaded worker system where a thread pool executes jobs from a
	list of scheduled jobs and callbacks.

******************************************************************************/

#include "MaxsiEngine.h"

BeginMaxsiNamespace

#define MAXSI_WORKER_JOB_LIST_SIZE 1024

MaxsiWorkerJob*		JobList			=	NULL;
MaxsiHandle			JobListLock		=	NULL; // A thread must own this before accessing any of these variables.
MaxsiHandle			JobListEvent	=	NULL; // Signalled when there are unread jobs.
size_t				JobListPointer	=	0; // The location of the current unread job.
size_t				JobListOffset	=	0; // The location of the next free job slot.
size_t				JobListUnread	=	0; // The number of unread jobs in the list.
size_t				JobListLength	=	0; // The number of entries in the list.
MaxsiHandle*		ThreadList		=	NULL;
size_t				NumThreads		=	0;
bool				ShouldMainLoop	=	false;

size_t GetNumWorkerThreads()
{
	return NumThreads;
}

size_t GetNumWorkerThreadsForNumCPUs(size_t NumCPUs)
{
	return 1 * NumCPUs; // 1 Thread/CPU * NumCPUs
}

MaxsiError CreateWorkerThreadPool()
{
	if ( ThreadList || JobListLock ) { return MAXSI_ERROR_ALREADY_INITIALIZED; }

	// Get the number of CPUs.
	size_t	NumCPUs		=	GetNumberOfCPUs();

	// Check if that went well.
	if ( NumCPUs == 0 ) { return MAXSI_ERROR_UNSPECIFIED; }

	MaxsiError	Result	=	MAXSI_ERROR_SUCCESS;

	if ( Result == MAXSI_ERROR_SUCCESS )
	{
		// Calculate how many threads we are going to use.
		NumThreads		=	GetNumWorkerThreadsForNumCPUs(NumCPUs);

		// Allocate a list of threads.
		ThreadList		=	new MaxsiHandle[NumThreads];

		// Check if that went well.
		if ( ThreadList == NULL ) { Result = MAXSI_ERROR_MEM_ALLOC_FAIL; }
	}

	if ( Result == MAXSI_ERROR_SUCCESS )
	{
		// Allocate a lock for our job list!
		JobListLock		=	ThreadSystem()->CreateCriticalSection();

		// And check if that went well.
		if ( JobListLock == NULL ) { Result = MAXSI_ERROR_UNSPECIFIED; }
	}

	if ( Result == MAXSI_ERROR_SUCCESS )
	{
		// Allocate an event that will be signaled when new jobs are available.
		JobListEvent	=	ThreadSystem()->CreateEvent();

		// And check if that went well.
		if ( JobListEvent == NULL ) { Result = MAXSI_ERROR_UNSPECIFIED; }	
	}

	if ( Result == MAXSI_ERROR_SUCCESS )
	{
		// Allocate the job list!
		JobListLength	=	MAXSI_WORKER_JOB_LIST_SIZE;
		JobList			=	new MaxsiWorkerJob[JobListLength];

		// Clean up in case of error conditions!
		if ( JobList == NULL ) {  delete[] ThreadList; return MAXSI_ERROR_MEM_ALLOC_FAIL; }
	}

	if ( Result == MAXSI_ERROR_SUCCESS )
	{
		memset(ThreadList, 0, sizeof(MaxsiHandle) * NumThreads);

		ShouldMainLoop	=	true;

		for ( size_t I = 0; I < NumThreads; I++ )
		{
			// Create each thread and make them all start in our Thread Main Loop!
			ThreadList[I]	=	ThreadSystem()->CreateThread(MaxsiWorkerMainLoop, (BYTE*)I);

			// We need at least one worker thread, otherwise we failed.
			if ( ThreadList[I] == NULL ) { if ( I == 0 ) { Result = MAXSI_ERROR_COULD_NOT_CREATE_THREAD; } NumThreads = I; break; }
		}
	}

	if ( Result != MAXSI_ERROR_SUCCESS && JobList != NULL ) { delete[] JobList; JobList = NULL; }
	if ( Result != MAXSI_ERROR_SUCCESS && JobListEvent != NULL ) { ThreadSystem()->DeleteEvent(JobListEvent); JobListEvent = NULL; }
	if ( Result != MAXSI_ERROR_SUCCESS && JobListLock != NULL ) { ThreadSystem()->DeleteCriticalSection(JobListLock); JobListLock = NULL; }
	if ( Result != MAXSI_ERROR_SUCCESS && ThreadList != NULL ) { delete[] ThreadList; ThreadList = NULL; }

	return Result;
}

MaxsiError DeleteWorkerThreadPool()
{
	return MAXSI_ERROR_NOT_IMPLEMENTED;
}

Word_t MaxsiWorkerMainLoop(BYTE* Parameter)
{
	size_t ThreadId = (size_t)Parameter;

	// Listen for events on the job list.
	while ( ShouldMainLoop && ThreadSystem()->WaitForEvent(JobListEvent) )
	{
		// Pthreads requires us to unlock the wait to allow other to access the event.
		ThreadSystem()->WaitedForEvent(JobListEvent);

		bool	FoundAJob	=	true;

		while ( FoundAJob )
		{
			MaxsiWorkerJob	MyJob;

			// Lock the job list to detect a job for us to perform.
			ThreadSystem()->EnterCriticalSection(JobListLock);

			// Detect if there is any unread jobs, if there is, then read it.
			if ( JobListUnread > 0 )
			{
				// Copy the job off the list to the stack.
				MyJob			=	JobList[JobListPointer];

				// Reset the job entry (actually, we don't need this, but it could be useful for debugging purposes).
				MAXSI_TODO("Find out whether this should only go into debug builds, or what?");
#ifdef Maxsi_Debug_Build
				memset(JobList[JobListPointer], 0, sizeof(JobList[JobListPointer]));
#endif

				// Progress the job list pointer.
				JobListPointer	=	(JobListPointer + 1) % JobListLength;

				// Decrease the number of unread jobs.
				JobListUnread--;
			}
			else
			{
				// Mark that we are out of jobs!
				FoundAJob		=	false;
			}
		
			// Unlock the job list as fast as we can, so other threads can have fun with it.
			ThreadSystem()->LeaveCriticalSection(JobListLock);		

			// Check if we found a job, otherwise go back to waiting.
			if ( FoundAJob )
			{
				// Attempt to execute the job.
				MaxsiError	Result	=	MyJob.Function(ThreadId, MyJob.Parameter);

				if ( Result != MAXSI_ERROR_SUCCESS )
				{	
					// It appears something awful happened!
					MAXSI_TODO("Should we add error handling here? We could, for instance, shut down all worker threads and exit the mainloop.");
					MAXSI_TODO("We could also allow a return value that tells us that the function will block and that if we have anything else to do, we should do that instead.");
				}
			}
		}
	}

	return 0;
}

MaxsiError ScheduleWorkerThreadJob(MaxsiWorkerCallback Function, BYTE* Parameter)
{
	if ( JobListLock == NULL ) { return MAXSI_ERROR_NOT_INITIALIZED; }

	MaxsiError	Result	=	MAXSI_ERROR_SUCCESS;

	// Lock the job list so that we can add to it.
	ThreadSystem()->EnterCriticalSection(JobListLock);

	// Check if our buffer is large enough to hold another job.
	if ( JobListUnread == JobListLength )
	{
		// Oh no! It appears we have filled our job list. We need to allocate more buffer space.
		// This shouldn't happen, but can. If there is a bug in the reading threads, the buffer
		// will easily overflow and cause this code to be hit over and over again until the system
		// doesn't have enough memory and the allocation failes. That would suck.

		MAXSI_TODO_LOW("When the job list grows very, very large (100's of MiBs or GiBs!), this memory allocation (and later deletion) will cause the job list to be locked while the memory allocation goes on (which may take a very, very long time) and will cause all other threads to block while waiting for permission to access the job list!");

		// Allocate a larger, bigger, and better job list!
		MaxsiWorkerJob*		NewJobList	=	new MaxsiWorkerJob[JobListLength + MAXSI_WORKER_JOB_LIST_SIZE];

		// Check if we don't have enough room in our buffer for another job.
		if ( NewJobList == NULL ) { Result = MAXSI_ERROR_OUT_OF_MEM; }

		if ( Result == MAXSI_ERROR_SUCCESS )
		{
			// Oh, and while we're at it, steal everything from the old list!
			if ( JobListOffset <= JobListPointer )
			{
				// We have to do two copies because the data is not sequential here.
				// (Since the buffer is full when this code is run, this case is the
				// most likely one to happen.)

				// Declaring the variables here aids in debugging. Though, it has now been shown
				// this code is bug free. The compiler optimizes all this away regardless.
				
				size_t DestOff = 0;
				size_t SrcOff = JobListPointer;
				size_t Num = (JobListLength-JobListPointer);

				memcpy(NewJobList + DestOff, JobList + SrcOff, sizeof(MaxsiWorkerJob) * Num);
				
				DestOff = (JobListLength-JobListPointer);
				SrcOff = 0;
				Num = JobListOffset;

				memcpy(NewJobList + DestOff, JobList + SrcOff, sizeof(MaxsiWorkerJob) * Num);
			}
			else
			{
				// We can get away with just one copy, yay!
				memcpy(NewJobList, JobList + JobListPointer, sizeof(MaxsiWorkerJob) * JobListUnread);			
			}

			// Now change these variables to reflect the new state of business.
			JobListPointer	=	0;
			JobListOffset	=	JobListUnread;	
			JobListLength	=	JobListLength + MAXSI_WORKER_JOB_LIST_SIZE;	

			delete[] JobList;
			JobList = NewJobList;

#ifdef Maxsi_Debug_Build
			// This is useful to debug jobs that trigger job-based fork bombs (when jobs multiply like rabbits).
			//printf("Increased job list to Default*%zu!\n", JobListLength/MAXSI_WORKER_JOB_LIST_SIZE );
#endif
		}

	}
	
	if ( Result == MAXSI_ERROR_SUCCESS )
	{
		// Set the new job.
		JobList[JobListOffset].Function		=	Function;
		JobList[JobListOffset].Parameter	=	Parameter;

		// Progress the offset of the next free slot.
		JobListOffset	=	(JobListOffset + 1) % JobListLength;

		// Increase the number of unread jobs.
		JobListUnread++;
	}

	// Unlock the job list as fast as we can, so other threads can have fun with it.
	ThreadSystem()->LeaveCriticalSection(JobListLock);

	if ( Result == MAXSI_ERROR_SUCCESS )
	{
		// Signal to a worker thread that we have an unread job!
		ThreadSystem()->SignalEvent(JobListEvent);
	}
	
	return Result;
}

EndMaxsiNamespace

